I am testing a react component which contains a form(using formik) and I need to test if on submit button clicked, whether submit function is called or not.
at the moment, the test is failing.
now, the form has required fields schema too using yup
so, I was wondering whether I need to fill up all the fields before testing it.
because at the moment, it doesnt submit until the form has errors i.e. if the required fieldss are empty. so does that obstruct the testing of the button click and the function being called or not?
describe('submitform', () => {
let wrapper = '';
const handleSubmit = jest.fn();
beforeEach(() => {
wrapper = mount(<ExampleButton >
<span className="visible-sm">Next</span>
<span className="visible-xs font-entity">
›
</span>
</ExampleButton>
);
});
afterEach(() => {
wrapper.unmount();
});
it('call function on click',async ()=> {
// let btn = wrapper.find('#btnEx').find('button').find('#btnEx');
let btn = wrapper.find('button').simulate('click');
console.log('wrapper : ',btn.debug());
// btn.props().onClick();
expect(handleSubmit).toHaveBeenCalled();
});
})
how do I fill up the fields then, before testing? or is it even required for me to fill up the fields before testing on click?
You need a way to pass your mock handleSubmit function to your ExampleButton
If ExampleButton has onSubmit event handler prop, this is easier:
// ExampleButton.jsx
const ExampleButton = ({ onSubmit }) => <button type="submit" onClick={onSubmit} />;
// ExampleButton.test.jsx
const handleSubmit = jest.fn();
...
wrapper = mount(<ExampleButton onSubmit={handleSubmit} />);
If ExampleButton has inner event handler function, kinda tricky
// ExampleButton.jsx
const ExampleButton = () => {
const handleSubmit = (params) => {...}
return <button type="submit" onClick={handleSubmit} />;
}
// ExampleButton.test.jsx
wrapper = mount(<ExampleButton onSubmit={handleSubmit} />);
wrapper.find('button').simulate('click', mockParams);
Related
I'm working on implementation of a multi step form with react-hook-form and my problem is that input fields do not get reinitialized with the form data when I return to the previous page.
I'm using <FormProvider /> component from react-hook-form to inject the form data into the pages and my input components are registered with register method from useFormContext() hook
const CreateAccount = () => {
const [currentStep, setCurrentStep] = useState(0);
const methods = useForm<FormData>({
mode: "onChange",
});
const onSubmit = (data) => console.log(data);
const handleNextStep = () => {
if (currentStep >= 5) return;
setCurrentStep(currentStep + 1);
};
const handlePreviousStep = () => {
if (currentStep <= 0) return;
setCurrentStep(currentStep - 1);
};
const renderContent = () => ({
[RegistrationSteps.UsernameEmail]: <UsernameEmail handleNextStep={handleNextStep} handlePreviousStep={handlePreviousStep} />,
[RegistrationSteps.Password]: <CreatePassword handleNextStep={handleNextStep} handlePreviousStep={handlePreviousStep} />,
});
return (
<Container maxWidth="sm">
<FormProvider {...methods}>
<form onSubmit={methods.handleSubmit(onSubmit)}>
{renderContent()[currentStep]}
</form>
</FormProvider>
</Container>
);
};
export default CreateAccount;
Here is what the input fields look like
const {
register
} = useFormContext();
<TextField
label="Email"
{...register("email")}
/>
Even though the form still holds the data in its state, it does not populate into corresponding fields when I switch back and forth between the form pages.
Instead of a single form at a global level, I recommend creating each component in your step as a form with its own instance of useForm() and wrapping steps in a state provider to store data across different steps. That way, you can assign values to the step forms from the respective state using defaultValues option of useForm on initialization.
You can check out this for the basic architecture that I'm trying to explain.
defaultValues in useForm
I'm using a shorten URL API when the user passes a valid link, i fetch API and render the shortened URL with "map medthod" to make them into a list. It has a btn next to each mapped "shortened URL" where onClick i try to copyToClipboard and change state of btn from Copy to Copied. The problem is currently it only works fine if i have 1 item(on click btn works fine with copyToClipboard) but if i have 2 buttons and i click the very 1st btn to copyToClipboard it's focusing the last item in mapped list and copying the value of (last item) 2nd btn and also setting state for all btns to copied. I also don't understand why i can't see li tags with keys in console when i pass them the keys. can someone help me out. I just want to copyToClipboard that input value of the btn i have clicked. here's what it looks like - image of onCLick of 1st btn 2nd btn gets focus & image of no keys in console & apparently they aren't in a list?
Here is the code below
import { useForm } from "react-hook-form";
import axios from 'axios';
import Loading from '../../images/Ripple-1s-200px.svg';
const Shorten = () => {
// get built in props of react hook form i.e. register,handleSubmit & errors / watch is for devs
const { register, handleSubmit, formState: {errors} } = useForm();
//1. set user original values to pass as params to url
const [link, setLink] = useState('');
//2. set loader initial values to false
const [loading, setLoading] = useState(false);
//3. pass the fetched short link object into an array so we can map
const [displayLinks, setDisplayLinks] = useState([]);
//4. onSubmit form log data into link & showLoader for a breif moment
const onSubmit = (data, event) => {
event.preventDefault();
//puttin data in a variable to pass as url parameter if valid
setLink(data.userLink);
//add loading here after data is set to state
setLoading(!false);
}
//5. fetch the shortened url link using async method to show loading
useEffect(() => {
let unmounted = false;
async function makeGetRequest() {
try {
let res = await axios.get('https://api.shrtco.de/v2/shorten', { params: { url: link } });
//hid loader if u get response from api call
if (!unmounted && res.data.result.original_link) {
setLoading(false);
//add the data to displayLinks array to map
return setDisplayLinks(displayLinks => [...displayLinks, res.data.result]);
}
}
catch (error) {
console.log(error, "inital mount request with no data");
}
}
//invoke the makeGetRequest here
makeGetRequest();
return () => {
unmounted = true;
}
//passing dependency to re-render on change of state value
}, [link]);
//6. intial State of copied or not button
const [copySuccess, setCopySuccess] = useState('Copy');
const inputRef = useRef(null);
//7. onCick of button target it's short url right now it's selecting the last element
const copyToClipboard = (e) => {
e.preventDefault();
inputRef.current.select();
document.execCommand('copy');
// This is just personal preference.
setCopySuccess('Copied');
};
console.log(displayLinks);
return (
<div>
<form onSubmit={handleSubmit(onSubmit)}>
<label></label>
<input
{...register("userLink", {required: "Please add a link"})}
type="url"
id="userLink"
/>
{errors.userLink && <span>{errors.userLink.message}</span>}
<input type="submit" />
</form>
{
loading ?
<div className="loader" id="loader">
<img src={Loading} alt="Loading" />
</div>
: <ul>
{
displayLinks.map((el) => {
return (
<li key={el.code}>
<div>
<h5>{el.original_link}</h5>
</div>
{
/* Logical shortcut for only displaying the
button if the copy command exists */
document.queryCommandSupported('copy') &&
<form>
<input
ref={inputRef}
defaultValue={el.full_short_link}>
</input>
<button onClick={copyToClipboard}>{copySuccess}</button>
</form>
}
</li>
)
})
}
</ul>
}
</div>
)
}
export default Shorten;
Its because you are using a single ref for all the links
You are looping over all the links and giving their <input ref={inputRef} />.So the ref will always be attached to the last link input
Maybe don't use refs and use an alternative copyToClipboard function
like this one
const copyToClipboard = (url) => {
const textField = document.createElement('textarea')
textField.innerText = url
document.body.appendChild(textField)
if (window.navigator.platform === 'iPhone') {
textField.setSelectionRange(0, 99999)
} else {
textField.select()
}
document.execCommand('copy')
textField.remove()
setCopySuccess('Copied');
}
OR
Use a library like react-copy-to-clipboard
Also please go through this link
I am attempting to test that a callback function is being called by submitting a form. I have mocked an onSubmit function which is passed into react-final-form. As shown in the codesandbox and below, I've got a simple form with an onSubmit callback.
export const MyForm = ({ onSubmit }) => (
<Form
onSubmit={onSubmit}
render={({ handleSubmit }) => (
<form onSubmit={handleSubmit} autoComplete="off">
<Field
label="Email"
component={Input}
name="email"
type="email"
autoComplete="off"
/>
<button>Submit</button>
</form>
)}
/>
);
When I simulate a click event on the button, I expect it to call the mocked function.
it("should call onSubmit when the button is click", () => {
const button = wrapper.find("button");
expect(button.length).toBeGreaterThan(0);
button.at(0).simulate("click");
expect(mockSubmit).toHaveBeenCalled();
});
Any assistance would be greatly appreciated.
Codesandbox
You will need to simulate submit in order to submit the form.
As for the Warning: An update to ReactFinalForm inside a test was not wrapped in act(...)., you are using a promise in your submit handler in the test which causes the form validation, submit, and state updates to be async.
act() provides a scope around expected component updates, and you will get this warning when an component does something outside of this scope. Since in the test the submit handler is async, the updates will happen outside of the act() function and will give you this error.
There are two ways to fix this, make the submit handler sync via jest.fn().
const mockSubmit = jest.fn();
If you need to keep this async, you will need to act/await over a the submit promise. This would mean you would need to create a resolved promise value and have a mock function resolve it.
const promise = Promise.resolve();
const mockSubmit = jest.fn(() => promise);
beforeEach(() => {
wrapper = mount(<MyForm onSubmit={mockSubmit} />);
});
it("should call onSubmit when the button is click 2", async () => {
const button = wrapper.find("form");
expect(button.length).toBeGreaterThan(0);
button.at(0).simulate("submit");
expect(mockSubmit).toHaveBeenCalled();
await act(() => promise);
});
My preferred method is to use <button type="submit">Submit</button> and then fireEvent.click(getByText('Submit')), like this.
In my parent component i have a function called handleDocumentSubmission which i want to pass down to the child.
handleDocumentSubmission = input => async (e) => {
console.log("fired");
I then render the following component like this. I use the same function name.
<FrontConfirm
nextStep={this.nextStep}
handleReview={this.handleReview}
values={values}
handleDocumentSubmission={this.handleDocumentSubmission}
/>
Now in my child component i want to call this function from a child function on click of a button.
continue = () => {
console.log("clicked", this.props);
this.props.handleDocumentSubmission("front");
};
<Button onClick={this.continue}
variant="contained" color="primary">
Confirm
</Button>
Now the console log for clicked i can see with props that has my handleDocumentSubmission function. But the console.log from the parent function console.log("fired") Does not get called.
This happens because handleDocumentSubmission is a curried function that accepts 2 sets of parameters. By using the following syntax and passing your event param, it will work :
continue = ev => {
console.log("clicked", this.props);
this.props.handleDocumentSubmission("front")(ev);
};
Your function will also not need to be asynchrinous :
handleDocumentSubmission = input => e => {
console.log("fired");
}
The final syntax without the continue function (I assume you created it for testing) will be the following :
<Button onClick={this.props.handleDocumentSubmission("front")}
variant="contained" color="primary">
Confirm
</Button>
Using this, your function will receive both your value (front) and the event information when fired.
Having a synchronous function will not prevent it from returning a value :
handleDocumentSubmission = input => e => {
console.log("fired");
return 'success'
}
continue = ev => {
console.log("clicked", this.props);
const result = this.props.handleDocumentSubmission("front")(ev);
console.log(result)
};
If you really want it to be async, use the await keyword :
handleDocumentSubmission = input => async e => {
console.log("fired");
return /* A promise */
}
continue = async ev => {
console.log("clicked", this.props);
const result = await this.props.handleDocumentSubmission("front")(ev);
console.log(result)
};
I have a INPUT BUTTON and INPUT FILE, I want to click the BUTTON and it will trigger the INPUT FILE event in REACT JS.
React.createElement('input',{type:'file', name:'myfile'})
then the button
React.createElement('a',{onClick: this.doClick},'Select File')
So how to define and trigger the INPUT FILE click event when we click the A HREF?
Your help is appreciate.
:-)
Update: Sep 18, 2021
Note: On NextJS, I was facing onChange event is not trigged from input file element. For that, we can use onInputCapture or onChangeCapture. For more detailed information, Stackoverflow - onChange event is not firing
Basic example on onChangeCapture as per our requirement. Requires React ^16.8,
const Dummy = () => {
const inputFileRef = React.useRef();
const onFileChangeCapture = ( e: React.ChangeEvent<HTMLInputElement> ) {
/*Selected files data can be collected here.*/
console.log(e.target.files);
};
const onBtnClick = () => {
/*Collecting node-element and performing click*/
inputFileRef.current.click();
};
return (
<form>
<input
type="file"
ref={inputFileRef}
onChangeCapture={onFileChangeCapture}
/>
<button onClick={onBtnClick}>Select file</button>
</form>
);
};
Using useRef Hook in functional components. Requires React ^16.8,
const Dummy = () => {
const inputFileRef = useRef( null );
const onFilechange = ( e ) => {
/*Selected files data can be collected here.*/
console.log( e.target.files );
}
const onBtnClick = () => {
/*Collecting node-element and performing click*/
inputFileRef.current.click();
}
return (
<form className="some-container">
<input
type="file"
ref={inputFileRef}
onChange={onFileChange}
/>
<button onClick={onBtnClick}>Select file</button>
</form>
)
}
Class Implementation with React.createRef() and handling click with node element.
class Dummy extends React.Component {
constructor( props ) {
super( props );
this.inputFileRef = React.createRef();
this.onFileChange = this.handleFileChange.bind( this );
this.onBtnClick = this.handleBtnClick.bind( this );
}
handleFileChange( e ) {
/*Selected files data can be collected here.*/
console.log( e.target.files );
}
handleBtnClick() {
/*Collecting node-element and performing click*/
this.inputFileRef.current.click();
}
render() {
return (
<form className="some-container">
<input
type="file"
ref={this.inputFileRef}
onChange={this.onFileChange}
/>
<button onClick={this.onBtnClick}>Select file</button>
</form>
)
}
}
You don't need jQuery for this. You don't even need an event handler. HTML has a specific element for this, called label.
First, make sure your input element has an id attribute:
React.createElement('input',{type:'file', name:'myfile', id:'myfile'})
Then, instead of:
React.createElement('a',{onClick: this.doClick},'Select File')
Try:
React.createElement('label',{htmlFor: 'myfile'},'Select File')
(Instead of adding htmlFor and id attributes, another solution is to make the input element a child of the label.)
Now clicking the label should trigger the same behaviour as clicking the input itself.
You could trigger the input type file with ref, f.e:
on your class component:
<input
ref={fileInput => this.fileInput = fileInput}
type="file"
/>
<button onClick={this.triggerInputFile}> Select File </button>
and make a function on that class component too:
triggerInputFile = () => this.fileInput.click()
Using Hooks with useref:
import React, {useRef} from 'react';
const FancyInput = () => {
const fileInput = useRef(null)
const handleClick = () => {
fileInput.current.click()
}
const handleFileChange = event => {
console.log("Make something")
}
return(
<div className="patientactions-container">
<input
type="file"
onChange={(e) => handleFileChange(e)}
ref={fileInput}
/>
<div onClick={() => handleClick()}></div>
</div>
)
}
export default FancyInput;
Building on the answer from #YÒGÎ , here is an implementation using TypeScript:
class Dummy extends React.Component {
fileInputRef: React.RefObject<HTMLInputElement> = React.createRef();
forwardClickToInputElement = () => {
this.fileInputRef.current!.click();
};
handleUploadDemand = (ie: ChangeEvent<HTMLInputElement>) => {
const fileList: FileList = ie.target.files;
// do something with the FileList, for example:
const fileReader = new FileReader();
fileReader.onload = () => {
const str = String(fileReader.result);
try {
const parsedContent = YOUR_OWN_PARSING(str);
} catch (error) {
// YOUR OWN ERROR HANDLING
}
};
fileReader.readAsBinaryString(fileList[0])
}
render() {
return (
<div className="some-container">
<button onClick={this.forwardClickToInputElement}>Select File</button>
<input ref={this.fileInputRef} type="file" onChange={this.handleSelectFile} hidden={true}/>
</div>
)
}
}
References:
Solution for how to use refs in React with Typescript https://stackoverflow.com/a/50505931/2848676
Use ! operator for ref type narrowing https://medium.com/#martin_hotell/react-refs-with-typescript-a32d56c4d315
const CustomInput = () => {
const handleClick = () => {
document.getElementById("file_upload").click();
};
const handleFileChange = (event) => {
console.log("Make something");
};
return (
<div className="patientactions-container">
<input type="file" id="file_upload" onChange={(e) => handleFileChange(e)} />
<div onClick={() => handleClick()}></div>
</div>
);
};
export default CustomInput;
EDIT: This is a question I answered a long time ago not knowing very much react at this time. The fun thing is that it has been considered valid ^^.
So for anyone reading this answer; this answer is wrong and is a very good example of something you shouldn't do in react.
Please find below a nice anti-pattern, again, don't do it.
=================================================
You can achieve this using jQuery:
this.doClick: function() {
$('input[type=file]').trigger('click');
}
React does not provide specific functions to trigger events, you can use jQuery or simply native Javascript: see Creating and triggering events on MDN