How to simulate/test key press using Jest? - reactjs

I'm trying to simulate/test the keypress event which calls props.onClick. I successfully simulated the onClick with similar testing code, but the keypress doesn't work.
When I tested it manually by pressing the Enter key, it works as it should, but when I try to write tests for it, it keeps failing and I'm not sure why. I put a logger in the useEffect function, and it gets called during the test, but it fails and gives an error that onClickFnc was never called.
In my button function component, I bind an event listener:
button.js
useEffect(() => {
if (enterEnable) {
window.addEventListener('keydown', onEnterPress);
return () => window.removeEventListener('keydown', onEnterPress);
}
});
const onEnterPress = e => {
if (e.which == 13) {
onClickSync(e);
}
};
const onClickSync = e => {
props.onClick(e);
};
button.test.js
it('check ButtonLoader enable enter to work', () => {
const onClickFnc = jest.fn();
const wrapper = shallow(<ButtonLoader {...props} enterEnable={true} onClick={onClickFnc}/>);
wrapper.find('button').simulate('keypress', {key: 'Enter'})
expect(onClickFnc).toBeCalled();
});
Error:
expect(jest.fn()).toBeCalled()
Expected number of calls: >= 1
Received number of calls: 0
How can I successfully test this?

Try this instead
wrapper.find('button').simulate('keydown', {which: 13})
From what I have seen, simulate doesn't create an event object from the arguments. Instead it passes the object as is.

try with this code
element.simulate('keydown', { key: 'Enter' });
this worked fine for me. hope it also works for you.

Related

I can't set focus on the input

I have this component https://stackblitz.com/edit/react-ts-u7rw2w?file=App.tsx
When I click and call toggleInputCardName, I see in the console "NO inputRef.current null false". At the same time, an input is rendered on the page and above it I see "null inputRef true".
I tried using useEffect, but the result is the same.
How to understand this? IsInputCardName is simultaneously true and false?
How to set focus on the input in toggleInputCardName?
Try useCallback
const inputRef = useCallback(node => {
if (node) {
node.focus();
node.setSelectionRange(node.value.length, node.value.length);
}
}, []);
It is because the setState is async.
I think about 2 possibilities.
First one :
const toggleInputCardName = () => {
setInputCardNameVisibity(!isInputCardName);
};
React.useEffect(() => {
if (inputRef.current) {
console.log('YES inputRef.current', inputRef.current);
inputRef.current.focus();
inputRef.current.select();
} else {
console.log('NO inputRef.current', inputRef.current, isInputCardName);
}
}, [isInputCardName]);
Second one, you could simply add autofocus on the input and don't use ref :
<input
className="input-card-title"
type="text"
value="value"
autoFocus
/>
You need useLayoutEffect:
const toggleInputCardName = () => {
setInputCardNameVisibity(!isInputCardName)
}
React.useLayoutEffect(() => {
if (!inputRef.current) return
inputRef.current.focus()
inputRef.current.select()
}, [isInputCardName])
The reason it doesn't work in the handler or in a plain useEffect is that they are executing before the input is actually written to the DOM. State changes in react are flushed later, and don't happen immediately. useLayoutEffect waits until the DOM change is committed.
Another way of doing it is by wrapping it in a 0 second timeout. This looks hackier than it is, as a timer with 0 as the time to wait, will just wait until the current call stack is finished before executing.
const toggleInputCardName = () => {
setInputCardNameVisibity(!isInputCardName)
setTimeout(() => {
if (inputRef.current) {
console.log('YES inputRef.current', inputRef.current)
inputRef.current.focus()
inputRef.current.select()
} else {
console.log('NO inputRef.current', inputRef.current, isInputCardName)
}
}, 0)
}
But I'd probably useLayoutEffect.
#OneQ answer of using the focus attribute also makes a lot of sense and reduces noise.

React confirm alert handle key press = enter

import { confirmAlert } from "react-confirm-alert"; // Import
import "react-confirm-alert/src/react-confirm-alert.css"; // Import css
export default function App() {
const handleButtonPress = (event) => {
if (event.key === "Enter") {
alert("Click Yes");
}
};
const handleClick = (e) => {
confirmAlert({
title: "Confirm to submit",
message: "Are you sure to do this.",
buttons: [
{
label: "Yes",
onClick: () => {
alert("Click Yes");
},
onKeyPress: () => {
handleButtonPress();
}
},
{
label: "No",
onClick: () => {
alert("Click No");
}
}
]
});
};
return (
<div className="App">
<button
onClick={() => {
handleClick();
}}
>
Click
</button>
</div>
);
}
I'm testing react-confirm-alert.
I'm trying to handle Button Yes by pressing Enter. Both function onClick() for yes and no are working good, but press enter is not working.
Can someone let me know if I did something wrong?
There's several issues. This react-confirm-alert library looks like a poor library, I'd pick a different one if you can find one that suits your needs.
You're calling handleButtonPress() with no arguments, and then you're trying to read from the event object, which you don't pass.
const handleButtonPress = (event) => {
if (event.key === "Enter") {
The event.key line should be throwing an error since event is undefined. Since you're not seeing errors, it's clear this line isn't getting called. You should be using console.log or the debugger to double check what code is getting called.
You should also get in the habit of reading documentation. In this case, the documentation shows that onKeyPress is a top level setting, while you've incorrectly put it in buttons.
Either way, react-confirm-alert doesn't pass the event to the onKeyPress callback: https://github.com/GA-MO/react-confirm-alert/#options so it doesn't seem like this API should exist. It doesn't have any use.
I would either pick a different library, otherwise you'll need to add your own keypress listener to the document, and manually handle the enter key there.

Test a component with useState and setTimeout

Code structure is as same as given below:
FunctionComponent.js
...
const [open, handler] = useState(false);
setTimeout(() => {handler(true);}, 2000);
...
return (
...
<div className={active ? 'open' : 'close'}>
)
comp.test.js
jest.useFakeTimers();
test('test case 1', () => {
expect(wrapper.find('open').length).toBe(0);
jest.advanceTimersByTime(2000);
expect(wrapper.find('open').length).toBe(1);
jest.useRealTimers();
});
The problem is that the expression written in bold in test is saying the length of open class is still 0, so actual and expected are not meeting.
You want to test the outcome of the hook and not the hook itself since that would be like testing React. You effectively want a test where you check for if the open class exists and then doesn't exist (or vice versa), which it looks like you're trying.
In short, to solve your issue you need to use ".open" when selecting the class. I would also suggest using the .exists() check on the class instead of ".length()" and then you can use ".toBeTruthy()" as well.
You could look into improve writing your tests in a Jest/Enzyme combined format as well:
import { shallow } from 'enzyme';
import { FunctionComponent } from './FunctionComponent.jsx';
jest.useFakeTimers();
describe('<FunctionCompnent />', () => {
const mockProps = { prop1: mockProp1, prop2: mockProp2, funcProp3: jest.fn() };
const wrapper = shallow(<FunctionComponent {...mockProps} />);
afterEach(() => {
jest.advanceTimersByTime(2000);
});
afterAll(() => {
jest.useRealTimers();
});
it('should render as closed initially', () => {
expect(wrapper.find('.close').exists()).toBeTruthy();
// you could also add the check for falsy of open if you wanted
// expect(wrapper.find('.open').exists()).toBeFalsy();
});
it('should change to open after 2 seconds (or more)', () => {
expect(wrapper.find('.open').exists()).toBeTruthy();
// you could also add the check for falsy of close if you wanted
// expect(wrapper.find('.close').exists()).toBeFalsy();
});
});
EDIT: Sorry realised I wrote the test backwards after checking your code again, they should be fixed now.

Use toHaveBeenCalledWith to check EventTarget

I've built a custom Input React component (think wrapper) that renders an HTML input element. When a value is entered the change is passed to the parent component like this:
const handleChange = (event: SyntheticInputEvent<EventTarget>) => {
setCurrentValue(event.target.value);
props.onChange(event);
};
Everything works as expected but now I want to write a test for it:
it('Should render a Text Input', () => {
const onChange = jest.fn();
const { queryByTestId } = renderDom(
<Input type={InputTypesEnum.TEXT} name='car' onChange={onChange} />
);
const textInput = queryByTestId('text-input');
expect(textInput).toBeTruthy();
const event = fireEvent.change(textInput, { target: { value: 'Ford' }});
expect(onChange).toHaveBeenCalledTimes(1);
});
This works fine too except that I want to add a final expect using toHaveBeenCalledWith. I've tried several things but can't figure out how to do it. Any ideas?
Update: I've been reading this: https://reactjs.org/docs/events.html#event-pooling. It appears that if I change handleChange like this:
const handleChange = (event: SyntheticInputEvent<EventTarget>) => {
event.persist();
setCurrentValue(event.target.value);
props.onChange(event);
};
then the received object from onChange does change to include my test data. That said, I don't like the idea of altering an important feature of production code (in this case, event pooling) simply to accommodate a test.
You can do something like this with toHaveBeenCalledWith
expect(onChange).toHaveBeenCalledWith(
expect.objectContaining({
target: expect.objectContaining({
value: "Ford"
})
})
);

React + Jest + Enzyme form onSubmit test fails unless I use timeout

I have a React form wrapper that takes an onSubmit prop of type function, and I want to test if it's being called with the correct form values. If I check it right away, it fails, but if I put the check behind a 100ms timeout, it passes.
So it appears as if there needs to be some kind of processing time for the functions to be executed, but the timeout is so janky...
Example
test('onSubmit function is called with correct form values', () => {
const defaults = {
email: 'superadmin#gmail.com',
password: 'soSoSecret!'
};
const submitFunction = jest.fn();
const wrapper = mount(<LoginForm onSubmit={submitFunction} />);
// input form values
wrapper
.find('input[name="email"]')
.simulate('change', { target: { value: defaults.email } });
wrapper
.find('input[name="password"]')
.simulate('change', { target: { value: defaults.password } });
expect(submitFunction.mock.calls.length).toBe(0);
wrapper.find('form button[type="submit"]').simulate('click');
// if I test right away, it fails, but if I set timeout for 100ms, it passes
// does the form take time to process and the function be called????
jest.useFakeTimers();
setTimeout(() => {
expect(submitFunction.mock.calls.length).toBe(1);
const args = submitFunction.mock.calls[0][0];
expect(args).toEqual(defaults);
}, 100);
});
Is it possible to write a test like this without a timeout? Seems like an issue to me.
Thanks for reading! :D
One reason that I could think of is that you are trying to test something that happens asynchronously, and not right away. In that case, you can try using async await
PFB example:
test('onSubmit function is called with correct form values', async () => {
const defaults = {
email: 'superadmin#gmail.com',
password: 'soSoSecret!'
};
const submitFunction = jest.fn();
const wrapper = mount(<LoginForm onSubmit={submitFunction} />);
// input form values
wrapper
.find('input[name="email"]')
.simulate('change', { target: { value: defaults.email } });
wrapper
.find('input[name="password"]')
.simulate('change', { target: { value: defaults.password } });
expect(submitFunction.mock.calls.length).toBe(0);
await wrapper.find('form button[type="submit"]').simulate('click');
expect(submitFunction.mock.calls.length).toBe(1);
const args = submitFunction.mock.calls[0][0];
expect(args).toEqual(defaults);
});
Notice the async keyword before the test case callback begins, and the await keyword before the submit button click is simulated.

Resources