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
Related
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.
I have a modal with a list of answers.
I can either click an answer to select it, then click a button to confirm my choice.
Or I can double-click an answer to select it and confirm.
I'm having trouble properly handling the double-click case.
With React class components, I would have used setState()'s callback like this:
setState({selectedAnswer: answer}, confirm)
But right now, I only figured out the following:
const MyModal = ({hide, setAnwser}) => {
const [selectedAnswer, setSelectedAnswer] = useState(null);
const [isSelectionDone, setIsSelectionDone] = useState(false);
const confirm = () => {
if (!selectedAnswer) {
return;
}
setAnwser(selectedAnswer);
hide();
};
const handleAnswerOnClick = (answer) => {
setSelectedAnswer(answer);
};
const handleAnswerOnDoubleClick = (answer) => {
setSelectedAnswer(answer);
setIsSelectionDone(true);
};
useEffect(confirm, [isSelectionDone]);
return (
<div>
<div>{answers.map((answer) => <MyAnswer
isSelected={answer.id === selectedAnswer?.id}
key={answer.id}
answer={answer}
onClick={handleAnswerOnClick}
onDoubleClick={handleAnswerOnDoubleClick}/>)}</div>
<button onClick={confirm}>Confirm</button>
</div>
);
}
I strongly suspect that there's a nicer/better way of doing it.
Maybe a simple:
const MyModal = ({hide, setAnwser}) => {
const [selectedAnswer, setSelectedAnswer] = useState(null);
const confirm = () => {
if (!selectedAnswer) {
return;
}
setAnwser(selectedAnswer);
hide();
};
const handleAnswerOnClick = (answer) => {
setSelectedAnswer(answer);
};
const handleAnswerOnDoubleClick = (answer) => {
setAnwser(answer);
hide();
};
return (
<div>
<div>{answers.map((answer) => <MyAnswer
isSelected={answer.id === selectedAnswer?.id}
key={answer.id}
answer={answer}
onClick={handleAnswerOnClick}
onDoubleClick={handleAnswerOnDoubleClick}/>)}</div>
<button onClick={confirm}>Confirm</button>
</div>
);
}
Which way is better?
There is no similar set state in hooks (which fires callback after state is set).
But, you could apply following refactor:
const confirm = (sAnswer = selectedAnswer) => {
if (!sAnswer) {
return;
}
setAnwser(sAnswer);
hide();
};
And then
const handleAnswerOnDoubleClick = (answer) => {
setSelectedAnswer(answer);
confirm(answer);
};
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.
When I use useEffect I can prevent the state update of an unmounted component by nullifying a variable like this
useEffect(() => {
const alive = {state: true}
//...
if (!alive.state) return
//...
return () => (alive.state = false)
}
But how to do this when I'm on a function called in a button click (and outside useEffect)?
For example, this code doesn't work
export const MyComp = () => {
const alive = { state: true}
useEffect(() => {
return () => (alive.state = false)
}
const onClickThat = async () => {
const response = await letsbehere5seconds()
if (!alive.state) return
setSomeState('hey')
// warning, because alive.state is true here,
// ... not the same variable that the useEffect one
}
}
or this one
export const MyComp = () => {
const alive = {}
useEffect(() => {
alive.state = true
return () => (alive.state = false)
}
const onClickThat = async () => {
const response = await letsbehere5seconds()
if (!alive.state) return // alive.state is undefined so it returns
setSomeState('hey')
}
}
When a component re-renders, it will garbage collect the variables of the current context, unless they are state-full. If you want to persist a value across renders, but don't want to trigger a re-renders when you update it, use the useRef hook.
https://reactjs.org/docs/hooks-reference.html#useref
export const MyComp = () => {
const alive = useRef(false)
useEffect(() => {
alive.current = true
return () => (alive.current = false)
}
const onClickThat = async () => {
const response = await letsbehere5seconds()
if (!alive.current) return
setSomeState('hey')
}
}
The best way to describe question is my code:
function EstateParamsList({ estateType, category }) {
const [isLoading, setIsLoading] = useState(true)
const [params, setParams] = useState({})
const [showPopUp, setShowPopUp] = useState(false)
useEffect(() => {
if (category && typeof category.id !== undefined) {
return db.collection(`dictionaries/ESTATE_PARAMS/${estateType}/${category.id}/params`).onSnapshot(response => {
const paramsObject = {}
response.forEach(param => {
paramsObject[param.id] = {
...convertParamObjetcToFieldsConfig(param.data()),
change: fieldChangedHandler
}
})
setParams(paramsObject)
setIsLoading(false)
})
} else {
setIsLoading(false)
}
}, [category])
console.log(params)
const fieldChangedHandler = (event, fieldIdentifier) => {
if(params)
console.log(params)
}
So i have params variable, that im init with object, that i'm getting async from firebase. Implementation of initializing you can see in useEffect method. For every object i want to pass ref for the function "fieldChangedHandler", for managing value of inputs.
fieldChangedHandler is a method of my EstateParamsList. But there i cant get value of params!
Question is WHY? I'm calling fieldChangedHandler only after everything was rendered, and async request was done.
Below is console log of params. Why in func is empty params?
Calling:
const renderParamsAsFields = params => {
const fields = []
for (const key in params) {
fields.push(<Field {...params[key]} changed={event => params[key].change(event, key)} />)
}
return fields.length ? fields : <div className='EstateParamsManager-EmptyValues'>Нет параметров</div>
}
Why not use curried function?
const createFieldChangedHandler = (
params,
fieldIdentifier
) => event => {
if (params) console.log(params);
};
function EstateParamsList({ estateType, category }) {
//... other code
useEffect(() => {
//...other code
change: createFieldChangedHandler(
paramsObject,
param.id
),
//...other code
}, [category, estateType]);//fixed missing dependency
When you call the change function you should already have the right values in scope:
<Field {...params[key]} changed={params[key].change} />