I'm writing a test for my parent class which contains a child component. The code coverage reports that I haven't covered test for child components props method. Below is my structure of my components
export const CreateSRFullScreenPanel: React.FC<Props> = ({
interPluginContext,
srType,
errorMessage,
}: Props) => {
const [disableButton, setDisableButton] = React.useState(false);
const [submitSR, setSubmitSR] = React.useState(false);
const returnToHomePage = (): void => {
getRouteClient()
.changeRoute(getActiveRegionBaseUrl(state.regionName));
};
const cancelOp = interPluginContext.isValid()
? CancelAction
: returnToHomePage;
...
<childSR disableButton={disableButton} onSubmitComplete={() =>
setSubmitSR(false)}/>
<Button
type={ButtonType.Submit}
buttonStyle={ButtonStyle.Primary}
onClick={cancelOp}
>
Cancel
</Button>
};
When I wrote a test like below I was getting undefined for method calls.
it("check props method gets called", () => {
const wrapper = mount(
<ParentSR {...props} />
);
console.log(wrapper.find(CreateSR).props().onSubmitComplete()); // undefined
console.log(wrapper.find(CreateSR).props().disableButton()); // true
});
Also, when I click on a cancel button cancelOp method gets called. How do I mock returnToHomePage method calls?
Related
I am building this project to try and improve my understanding of react :), so I am a n00b and therefore still learning the ropes of extracting components, states, props etc =)
I have a child Component DescriptionDiv, its parent component is PlusContent and finally the parent component is PlusContentHolder. The user types some input into the DescriptionDiv which then, using a props/callback passes the user input to the PlusContent.
My question/problem is: after setting useState() in the PlusContent component, I am after a button click in the PlusContentHolder component, returned with an undefined in the console.log.
How come I cannot read the useState() in the next parent component, the PlusContentHolder?
I know that useState() is async so you cannot straight up call the value of the state in the PlusContent component, but shouldn't the state value be available in the PlusContentHolder component?
below is my code for the DescriptionDiv
import './DescriptionDiv.css';
const DescriptionDiv = props => {
const onDescriptionChangeHandler = (event) => {
props.descriptionPointer(event.target.value);
}
return (
<div className='description'>
<label>
<p>Description:</p>
<input onChange={onDescriptionChangeHandler} type='text'></input>
</label>
</div>);
}
export default DescriptionDiv;
Next the code for the PlusContent comp
import React, { useState } from "react";
import DescriptionDiv from "./div/DescriptionDiv";
import ImgDiv from "./div/ImgDiv";
import "./PlusContent.css";
import OrientationDiv from "./div/OrientationDiv";
const PlusContent = (props) => {
const [classes, setClasses] = useState("half");
const [content, setContent] = useState();
const [plusContent, setPlusContent] = useState({
orientation: "left",
img: "",
description: "",
});
const onOrientationChangeHandler = (orientationContent) => {
if (orientationContent == "left") {
setClasses("half left");
}
if (orientationContent == "right") {
setClasses("half right");
}
if (orientationContent == "center") {
setClasses("half center");
}
props.orientationInfo(orientationContent);
};
const onDescriptionContentHandler = (descriptionContent) => {
props.descriptionInfo(setPlusContent(descriptionContent));
console.log(descriptionContent)
};
const onImageChangeHandler = (imageContent) => {
props.imageInfo(imageContent);
setContent(
<>
<OrientationDiv
orientationPointer={onOrientationChangeHandler}
orientationName={props.orientationName}
/> {/*
<AltDiv altPointer={onAltDivContentHandler} />
<TitleDiv titlePointer={onTitleDivContentHandler} /> */}
<DescriptionDiv descriptionPointer={onDescriptionContentHandler} />
</>
);
};
return (
<div className={classes}>
<ImgDiv imageChangeExecutor={onImageChangeHandler} />
{content}
</div>
);
};
export default PlusContent;
and lastly the PlusContentHolder
import PlusContent from "../PlusContent";
import React, { useState } from "react";
const PlusContentHolder = (props) => {
const onClickHandler = (t) => {
t.preventDefault();
descriptionInfoHandler();
};
const descriptionInfoHandler = (x) => {
console.log(x) // this console.log(x) returns and undefined
};
return (
<div>
{props.contentAmountPointer.map((content) => (
<PlusContent
orientationInfo={orientationInfoHandler}
imageInfo={imageInfoHandler}
descriptionInfo={descriptionInfoHandler}
key={content}
orientationName={content}
/>
))}
<button onClick={onClickHandler}>Generate Plus Content</button>
</div>
);
};
export default PlusContentHolder;
The reason why the descriptionInfoHandler() function call prints undefined in its console.log() statement when you click the button, is because you never provide an argument to it when you call it from the onClickHandler function.
I think that it will print the description when you type it, however. And I believe the problem is that you need to save the state in the PlusContentHolder module as well.
I would probably add a const [content, setContent] = useState() in the PlusContentHolder component, and make sure to call setContent(x) in the descriptionInfoHandler function in PlusContentHolder.
Otherwise, the state will not be present in the PlusContentHolder component when you click the button.
You need to only maintain a single state in the PlusContentHolder for orientation.
Here's a sample implementation of your use case
import React, { useState } from 'react';
const PlusContentHolder = () => {
const [orientatation, setOrientation] = useState('');
const orientationInfoHandler = (x) => {
setOrientation(x);
};
const generateOrientation = () => {
console.log('orientatation', orientatation);
};
return (
<>
<PlusContent orientationInfo={orientationInfoHandler} />
<button onClick={generateOrientation}>generate</button>
</>
);
};
const PlusContent = ({ orientationInfo }) => {
const onDescriptionContentHandler = (value) => {
// your custom implementation here,
orientationInfo(value);
};
return <DescriptionDiv descriptionPointer={onDescriptionContentHandler} />;
};
const DescriptionDiv = ({ descriptionPointer }) => {
const handleChange = (e) => {
descriptionPointer(e.target.value);
};
return <input type="text" onChange={handleChange} />;
};
I would suggest to maintain the orientation in redux so that its easier to update from the application.
SetState functions do not return anything. In the code below, you're passing undefined to props.descriptionInfo
const onDescriptionContentHandler = (descriptionContent) => {
props.descriptionInfo(setPlusContent(descriptionContent));
};
This shows a misunderstanding of the use of state. Make sure you're reading about "lifting state" in the docs.
You're also declaring needless functions, e.g. onDescriptionContentHandler in your PlusContent. The PlusContent component could just pass the descriptionInfoHandler from PlusContentHolder prop directly down to DescriptionDiv, since onDescriptionContentHandler doesn't do anything except invoke descriptionInfoHandler.
You may want to consider restructuring your app so plusContent state is maintained in PlusContentHolder, and pass that state down as props. That state would get updated when DescriptionDiv invokes descriptionInfoHandler. It'd subsequently pass the updated state down as props to PlusContent.
See my suggested flowchart.
I have a parent Container, I plan to pass inside different child components that will accept callback.
Container:
const Container = ({children}) => {
const [selection, setSelection] = useState([]);
const setSelection = (returnObject) => {
setSelection(prev => [...selection, returnObject])
}
return(
<StyledContainer>
{children}
<Button>Search {selection}</Button>
</StyledContainer>
)
}
Container will have different children that all accept callback:
<Container><Child1 callback={}></Container>
<Container><Child2 callback={}></Container>
<Container><Child3 callback={}></Container>
Is there a way to pass component as a child to Container and for that Child to be using setSelection function as prop for Child's callback prop? (without Redux)
Yes you can override any props passed to the children as follows:
const Container = ({children}) => {
const [selection, setSelection] = useState([]);
const setSelection = (returnObject) => {
setSelection(prev => [...selection, returnObject])
}
return(
<StyledContainer>
{React.cloneElement(children, {callback: setSelection } }
<Button>Search {selection}</Button>
</StyledContainer>
)
}
Then you can use it as:
<Container><Child1/></Container>
<Container><Child2/></Container>
<Container><Child3/></Container>
And inside of each Child component, you can call callback() as need it.
React.cloneElement will create a copy from children passing additional props in this case only callback is passed as a prop you can pass as many as you need as a new object.
Details: https://reactjs.org/docs/react-api.html#cloneelement
My goal is very simple. I am just looking to set my react context from within a reusable function-only (stateless?) react component.
When this reusable function gets called it will set the context (state inside) to values i provide. The problem is of course you can't import react inside a function-only component and hence I cannot set the context throughout my app.
There's nothing really to show its a simple problem.
But just in case:
<button onCLick={() => PlaySong()}></button>
export function PlaySong() {
const {currentSong, setCurrentSong} = useContext(StoreContext) //cannot call useContext in this component
}
If i use a regular react component, i cannot call this function onClick:
export default function PlaySong() {
const {currentSong, setCurrentSong} = useContext(StoreContext) //fine
}
But:
<button onCLick={() => <PlaySong />}></button> //not an executable function
One solution: I know i can easily solve this problem by simply creating a Playbtn component and place that in every song so it plays the song. The problem with this approach is that i am using a react-player library so i cannot place a Playbtn component in there...
You're so close! You just need to define the callback inside the function component.
export const PlaySongButton = ({...props}) => {
const {setCurrentSong} = useContext(StoreContext);
const playSong = () => {
setCurrentSong("some song");
}
return (
<button
{...props}
onClick={() => playSong()}
/>
)
}
If you want greater re-usability, you can create custom hooks to consume your context. Of course where you use these still has to follow the rules of hooks.
export const useSetCurrentSong = (song) => {
const {setCurrentSong} = useContext(StoreContext);
setCurrentSong(song);
}
It is possible to trigger a hook function by rendering a component, but you cannot call a component like you are trying to do.
const PlaySong = () => {
const {setCurrentSong} = useContext(StoreContext);
useEffect( () => {
setCurrentSong("some song");
}, []
}
return null;
}
const MyComponent = () => {
const [shouldPlay, setShouldPlay] = useState(false);
return (
<>
<button onClick={() => setShouldPlay(true)}>Play</button>
{shouldPlay && <PlaySong />}
</>
)
}
I have a component with a props that is an action called changeReason()
And then, I have a function called when I press a button. The function is:
const handleNext = React.useCallback(() => {
if (!reason) {
changeReason({
code: defaultReason,
value: defaultReason
});
}
}
I want to test that changeReason() prop is called, I'm trying to do a test like this:
const changeReason = jest.fn();
const rendered = render(
<TransactionReason
changeReason={changeReason}
reason={null}
/>
);
// When some button is pressed...
const reasonNext = rendered.getByTestId('reasonButton');
// it must fire an event
fireEvent.press(reasonNext);
const FakeFun = jest.spyOn(rendered, 'changeReason');
expect(FakeFun).toHaveBeenCalled();
I'm getting:
Cannot spy the handleNext property because it is not a function; undefined given instead
How could I test that I'm calling that action?
I have a component that, on button click sends the updated value to parent via props.OnValChange. This is implemented in the useEffect hook.
If I console log the useEffect I can see it being called. But in my test when I do expect(prop.OnValChange).toHaveBeenCalledTimes(1); it says it was called 0 times.
Component:
const MyComp = ({OnValChange}) => {
const [ val, setVal ] = useState(0);
useEffect(() => {
console.log("before");
OnValChange(val);
console.log("after");
}, [val]);
return (
<button onClick={() => setVal(val + 1)}>Count</button>
)
}
Test:
it("Sends val to parent when button is clicked", () => {
const prop = {
OnValChange: jest.fn();
}
const control = mount(<MyComp {...prop} />);
expect(prop.OnValChange).toHaveBeenCalledTimes(0);
control.find(button).simulate("click");
expect(prop.OnValChange).toHaveBeenCalledTimes(1);
})
useEffect will always be called once when the component is initially mounted, and will be called a second time when you trigger a button click, so the correct test should be like this
it("Sends val to parent when button is clicked", () => {
const prop = {
OnValChange: jest.fn();
}
const control = mount(<MyComp {...prop} />);
expect(prop.OnValChange).toHaveBeenCalledTimes(1);
control.find(button).simulate("click");
expect(prop.OnValChange).toHaveBeenCalledTimes(2);
})
If you are always 0 times, I suspect that it is a problem with the version of enzyme-adapter-react-16. When I switch the version to 1.13.0, there will be the same problem as you, you can try enzyme -adapter-react-16 updated to the latest version.