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.
Related
useState does not update the state immediately.
I'm using react-select and I need to load the component with the (multi) options selected according to the result of the request.
For this reason, I created the state defaultOptions, to store the value of the queues constant.
It turns out that when loading the component, the values are displayed only the second time.
I made a console.log in the queues and the return is different from empty.
I did the same with the defaultOptions state and the return is empty.
I created a codesandbox for better viewing.
const options = [
{
label: "Queue 1",
value: 1
},
{
label: "Queue 2",
value: 2
},
{
label: "Queue 3",
value: 3
},
{
label: "Queue 4",
value: 4
},
{
label: "Queue 5",
value: 5
}
];
const CustomSelect = (props) => <Select className="custom-select" {...props} />;
const baseUrl =
"https://my-json-server.typicode.com/wagnerfillio/api-json/posts";
const App = () => {
const userId = 1;
const initialValues = {
name: ""
};
const [user, setUser] = useState(initialValues);
const [defaultOptions, setDefaultOptions] = useState([]);
const [selectedQueue, setSelectedQueue] = useState([]);
useEffect(() => {
(async () => {
if (!userId) return;
try {
const { data } = await axios.get(`${baseUrl}/${userId}`);
setUser((prevState) => {
return { ...prevState, ...data };
});
const queues = data.queues.map((q) => ({
value: q.id,
label: q.name
}));
// Here there is a different result than emptiness
console.log(queues);
setDefaultOptions(queues);
} catch (err) {
console.log(err);
}
})();
return () => {
setUser(initialValues);
};
}, []);
// Here is an empty result
console.log(defaultOptions);
const handleChange = async (e) => {
const value = e.map((x) => x.value);
console.log(value);
setSelectedQueue(value);
};
return (
<div className="App">
Multiselect:
<CustomSelect
options={options}
defaultValue={defaultOptions}
onChange={handleChange}
isMulti
/>
</div>
);
};
export default App;
React don't update states immediately when you call setState, sometimes it can take a while. If you want to do something after setting new state you can use useEffect to determinate if state changed like this:
const [ queues, setQueues ] = useState([])
useEffect(()=>{
/* it will be called when queues did update */
},[queues] )
const someHandler = ( newValue ) => setState(newValue)
Adding to other answers:
in Class components you can add callback after you add new state such as:
this.setState(newStateObject, yourcallback)
but in function components, you can call 'callback' (not really callback, but sort of) after some value change such as
// it means this callback will be called when there is change on queue.
React.useEffect(yourCallback,[queue])
.
.
.
// you set it somewhere
setUserQueues(newQueues);
and youre good to go.
no other choice (unless you want to Promise) but React.useEffect
Closures And Async Nature of setState
What you are experiencing is a combination of closures (how values are captured within a function during a render), and the async nature of setState.
Please see this Codesandbox for working example
Consider this TestComponent
const TestComponent = (props) => {
const [count, setCount] = useState(0);
const countUp = () => {
console.log(`count before: ${count}`);
setCount((prevState) => prevState + 1);
console.log(`count after: ${count}`);
};
return (
<>
<button onClick={countUp}>Click Me</button>
<div>{count}</div>
</>
);
};
The test component is a simplified version of what you are using to illustrate closures and the async nature of setState, but the ideas can be extrapolated to your use case.
When a component is rendered, each function is created as a closure. Consider the function countUp on the first render. Since count is initialized to 0 in useState(0), replace all count instances with 0 to see what it would look like in the closure for the initial render.
const countUp = () => {
console.log(`count before: ${0}`);
setCount((0) => 0 + 1);
console.log(`count after: ${0}`);
};
Logging count before and after setting count, you can see that both logs will indicate 0 before setting count, and after "setting" count.
setCount is asynchronous which basically means: Calling setCount will let React know it needs to schedule a render, which it will then modify the state of count and update closures with the values of count on the next render.
Therefore, initial render will look as follows
const countUp = () => {
console.log(`count before: 0`);
setCount((0) => 0 + 1);
console.log(`count after: 0`);
};
when countUp is called, the function will log the value of count when that functions closure was created, and will let react know it needs to rerender, so the console will look like this
count before: 0
count after: 0
React will rerender and therefore update the value of count and recreate the closure for countUp to look as follows (substituted the value for count).This will then update any visual components with the latest value of count too to be displayed as 1
const countUp = () => {
console.log(`count before: 1`);
setCount((1) => 1 + 1);
console.log(`count after: 1`);
};
and will continue doing so on each click of the button to countUp.
Here is a snip from codeSandbox. Notice how the console has logged 0 from the intial render closure console log, yet the displayed value of count is shown as 1 after clicking once due to the asynchronous rendering of the UI.
If you wish to see the latest rendered version of the value, its best to use a useEffect to log the value, which will occur during the rendering phase of React once setState is called
useEffect(() => {
console.log(count); //this will always show the latest state in the console, since it reacts to a change in count after the asynchronous call of setState.
},[count])
You need to use a parameter inside the useEffect hook and re-render only if some changes are made. Below is an example with the count variable and the hook re-render only if the count values have changed.
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // Only re-run the effect if count changes
The problem is that await api.get() will return a promise so the constant data is not going to have it's data set when the line setUserQueues(queues); is run.
You should do:
api.get(`/users/${userId}`).then(data=>{
setUser((prevState) => {
return { ...prevState, ...data };
});
const queues = data.queues.map((q) => ({
value: q.id,
label: q.name,
}));
setUserQueues(queues);
console.log(queues);
console.log(userQueues);});
I have these two components. One is a modal and another is a button the should open that modal (it is opened by passing true to its isOpen prop.
The modal starts of with isOpen being false, then when the button is clicked, it changes that state to true.
When compiling it woks just fine as intended. In my tests though, it keeps returning false as the value of isOpen even after clicking the button.
These are my two components and the state he handles them:
const [isManagePaymentSourceShowing, setIsManagePaymentSourceShowing] = useState(false);
<ManageSubscriptionModal
isOpen={isManageSubscriptionShowing}
onClickX={closeManageSubscriptionModals}
onClickCancelSubscription={() => setIsSignInAgainShowing(true)}
data-test="amsp-manage-subscription-modal"
/>
<TextCtaButton
fontSize={["13px", "12px"]}
fontWeight="600"
textTransform="uppercase"
onClick={() => {
setIsManagePaymentSourceShowing(true);
console.log("Button was clicked ".repeat(100));
}}
data-test="amsp-manage-payment-source-button"
>
manage payment source
</TextCtaButton>
I've added that console log too and I see that the button does get clicked in the test.
This is the test I am trying to run:
const setup = (userSubscription = userSubscriptionFixture()) => {
return mount(
<Wrapper>
<AccountManagementSubscriptionPresentational
subscriptionProduct={undefined}
userSubscription={userSubscription}
/>
</Wrapper>
);
};
describe("Check modal display toggles work", () => {
let wrapper;
let modal;
let updatedModal;
let openButton;
let openFunction;
test("<ManagePaymentSourceModal>", () => {
wrapper = setup();
modal = wrapper.find(`[data-test="amsp-manage-payment-source-modal"]`);
openButton = wrapper.find(
`[data-test="amsp-manage-payment-source-button"]`
);
openFunction = openButton.prop("onClick") as () => void;
expect(modal.prop("isOpen")).toBeFalsy();
openFunction();
updatedModal = wrapper.find(
`[data-test="amsp-manage-payment-source-modal"]`
);
expect(updatedModal.prop("isOpen")).toBeTruthy();
});
});
That last expect always fails saying it expected true and got false instead.
EDIT: I've tried tuning the test into an async function thing mabe it's the state change that just takes too long, but that didn't work either. This is how I've tried it:
test("<ManagePaymentSourceModal>", async () => {
wrapper = setup();
modal = wrapper.find(`[data-test="amsp-manage-payment-source-modal"]`);
openFunction = wrapper
.find(`[data-test="amsp-manage-payment-source-button"]`)
.prop("onClick") as () => void;
expect(modal.prop("isOpen")).toBe(false);
await openFunction();
updatedModal = wrapper.find(
`[data-test="amsp-manage-payment-source-modal"]`
);
expect(updatedModal.prop("isOpen")).toBe(true);
});
Changing my test to th following (invoke instead of prop) has solved my issue
openFunction = wrapper
.find(`[data-test="amsp-manage-payment-source-button"]`)
.invoke("onClick") as () => void;
Hi I have a functional component as shown below:
import React, { useRef, useEffect, useState } from 'react';
const SomeComponent = ({ prop1, ...otherProps}) => {
const divRef = useRef();
useEffect(() => {
divRef.current.addEventListener('mousedown', mouseDownFunc);
}, []);
const mouseDownFunc = () => {
document.addEventListener('mousemove', (el) => {
// call some parent function
});
}
return (
<div
className='test-div'
ref={ divRef }>
</div>
);
};
How do I test a react functional component wherein addEventListener is added using ref inside useEffect which when triggered calls mouseDownFunc.
I'm new to react jest testing, little confused on how to do it.
Testing this sort of component can be tricky, but using #testing-library/react I think I was able to come up with something useful.
I did have to make some changes to your component to expose the API a bit, and I also made some changes so that it stops listening to the events on mouseup which may not be the specific event you want.
Here's the modified component:
// MouseDownExample.js
import React, { useEffect, useState } from "react";
export default ({ onMouseMoveWhileDown }) => {
const [x, setX] = useState(null);
const [listening, setListening] = useState();
// Replaced with mouse move function, should make sure we're unlistening as well
useEffect(() => {
if (listening) {
const onMouseMove = (event) => {
// call some parent function
onMouseMoveWhileDown(event);
console.log(event.clientX);
// purely for testing purposes
setX(event.clientX);
};
const onMouseUp = (event) => {
// stop listening on mouse up
// - you should pick whatever event you want to stop listening
// - this is global so it also stops when the mouse is outside the box
setListening(false);
};
document.addEventListener("mousemove", onMouseMove);
document.addEventListener("mouseup", onMouseUp);
return () => {
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("mouseup", onMouseUp);
};
}
}, [listening, onMouseMoveWhileDown]);
return (
<div
style={{
backgroundColor: "red",
width: 200,
height: 200
}}
className="test-div"
onMouseDown={() => {
// moved this inline, so no ref
setListening(true);
}}
>
X Position: {x}
</div>
);
};
I called out in comments the main differences.
And here's an example test:
// MouseDownExample.test.js
import React from "react";
import { fireEvent, render } from "#testing-library/react";
import MouseDownExample from "./MouseDownExample";
it("shouldn't trigger onMouseMoveWhileDown when mouse isn't down", () => {
const onMouseMoveWhileDown = jest.fn();
const { container } = render(
<MouseDownExample onMouseMoveWhileDown={onMouseMoveWhileDown} />
);
// Note: normally I would use `screen.getByRole` but divs don't have a useful role
const subject = container.firstChild;
fireEvent.mouseMove(
document,
// https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/MouseEvent
{
clientX: 200
}
);
// hasn't gone down yet
expect(onMouseMoveWhileDown).not.toHaveBeenCalled();
fireEvent.mouseDown(subject);
fireEvent.mouseUp(subject);
// went down then up before moving
fireEvent.mouseMove(document, {
clientX: 200
});
expect(onMouseMoveWhileDown).not.toHaveBeenCalled();
});
it("should trigger onMouseMoveWhileDown when mouse is down", () => {
const onMouseMoveWhileDown = jest.fn();
const { container } = render(
<MouseDownExample onMouseMoveWhileDown={onMouseMoveWhileDown} />
);
// Note: normally I would use `screen.getByRole` but divs don't have a useful role
const subject = container.firstChild;
fireEvent.mouseDown(subject);
fireEvent.mouseMove(document, {
clientX: 200
});
expect(onMouseMoveWhileDown).toHaveBeenCalledWith(
expect.objectContaining({ clientX: 200 })
);
});
What's happening here, is we're rendering the component, then firing events to ensure the onMouseMoveWhileDown function prop is called when we expect.
We have to do expect.objectContaining rather than just the object because it's called with a MouseEvent which contains other properties.
Another test we might want to add is an unmount test to ensure the listeners are no longer triggering events.
You can look at/experiment with this Code Sandbox with this component and the tests. Hope this helps 👍
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'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?