How to test onClick which programmatically fires file input - reactjs

I'm fairly new to testing. I want to test whether my onClick function fired properly or not.
Behaviour of onClick function is firing file input programmatically.
const Component = () => {
const onBtnClick = (e: SyntheticEvent) => {
e.stopPropagation();
inputFileRef?.current?.click();
};
return (
<Box data-testid="upload-wrapper">
<input
data-testid="upload-input"
type="file"
hidden
ref={inputFileRef}
onChangeCapture={onFileChangeCapture}
accept="image/jpeg,image/png,image/jpg"
tabIndex={-1}
/>
<Button data-testid="upload-btn" onClick={onBtnClick}>
Upload image
</Button>
)
}

I found the solution from react-dropzone testing source file. At least it is working for my case. Here is the link react-dropzone#src/index.spec.js regarding opening file dialog programmatically.
My code:
it('Should open file dialog', () => {
const onClickSpy = vi.spyOn(HTMLInputElement.prototype, 'click');
renderScreen({});
fireEvent.click(screen.getByTestId('image-upload-btn'));
expect(onClickSpy).toHaveBeenCalled();
});
[1]: https://github.com/react-dropzone/react-dropzone/blob/master/src/index.spec.js#L3371

Related

React File Upload

I just want to create simple file upload component on react. It will be something simple that after select the file and submit the upload button, it will give a massage with filename like "You uploaded fileName.txt". I have tried search so many sources but it's mostly more than I need, that would be great if anyone could help me about it. Thank you!
You can detect file selection using the onChange event listener, and you can access the name of the selected file via e.target.files which returns all of the files which have been selected, we'll take the first selected file and store its name value inside a state called selectedFileName.
during render we'll check if the selectedFileName state has value, we'll display it inside a p tag.
import React, { useState } from 'react';
export const App = () => {
const [selectedFileName, setSelectedFileName] = useState('');
const [displayFileName, setDisplayFileName] = useState(false);
const submitFile = () => setDisplayFileName(true);
const handleFileSelect = e => {
const selectedFile = e.target.files[0];
setDisplayFileName(false);
setSelectedFileName(selectedFile?.name);
};
return (
<div>
<input
type='file'
accept='*'
name='fileSelect'
onChange={handleFileSelect}
/>
{displayFileName ? (
<p>{ selectedFileName?`You Uploaded '${selectedFileName}'`:"Please select a file!"}</p>
) : (
<button value='Submit' onClick={submitFile}>
Submit File
</button>
)}
</div>
);
};

Test useRef's currents' properties using jest-enzyme

I am trying to test a hidden file inputs click using Jest-enzyme, for the following component.
const UploadField = ({handleFileUpload}) => {
const hiddenFileInputRef = React.useRef(null);
const handleChange = (event) => {
handleFileUpload(event.target.files[0]);
};
const handleClick = () => {
hiddenFileInputRef.current.click();
};
return (
<>
<input
type="file"
ref={hiddenFileInputRef}
onChange={handleChange}
style={{display: 'none'}}
/>
<div
onClick={handleClick}
className="upload-button-container">
upload file
</div>
</>
);
};
i tried the following test :
it('should call handle click of hidden fileinput on click of div', () => {
const useRefSpy = jest
.spyOn(React, 'useRef')
.mockReturnValueOnce({current: {click: jest.fn()}});
let divWrapper = wrapper.find(
'.upload-button-container'
);
divWrapper.simulate('click');
expect(useRefSpy).toBeCalledTimes(1);
});
It gives an error "TypeError: Cannot read property 'click' of null" . what am i doing wrong here?
Try to check if you were able find the right div by logging wrapperDiv.html() or debugging
I think you might have to pass the event like in this post. Simulating a Div Click on Enzyme and React

Is there a way to have 2 onSubmit events in React?

I was wondering if anyone could explain how I can have 2 onSubmit events in React? I have a button for clicking a form on submission, and it has a thank you message popping up, but I'm trying to clear the localStorage at the same the form is submitted. I have tried this:
const handleSubmit = (e) => {
e.preventDefault();
alert(`Thank you for your order! ^_^ Please check your texts for updates!`);
toggle()
};
const clearCartStorage = () => {
localStorage.clear();
}
<Form onSubmit={handleSubmit && clearCartStorage()}>
These work separately, but when I have the && only the clearCartStorage function will run without the handleSubmit pop up.
Make a new function which calls those other functions:
<Form onSubmit={(e) => {
handleSubmit(e);
clearCartStorage();
})>
Careful, you are invoking clearCartStorage when you create the Form component there.
<Form onSubmit={handleSubmit && clearCartStorage()}> ❌❌❌
It should take one function which can call multiple.
I would set it up like this. It's more common to keep the function definition out of the returned JSX.
const MyComponent = () => {
const handleSubmit = (e) => {
e.preventDefault();
clearCartStorage()
alertUser()
};
const clearCartStorage = () => {
localStorage.clear();
}
const alertUser = () => {
alert(`Thank you for your order! ^_^ Please check your texts for updates!`);
toggle()
}
return <Form onSubmit={handleSubmit}>
}
Neither of the responses worked. I should have posted my whole code, but what I did to fix it was create a separate component with the new function that runs the alert and clearStorage functions. Having everything on the same component within a modal div was not working.
So I basically have:
<Modal>
<FormComponent /> // my new function in here
</Modal>

Upload and save list of files with react-hook-form

I am building a website targeted mostly at browsers using Ionic React.
I am trying to use a react-hook-form to upload a list of files (among other data) and save them in a FieldArray together with other data.
I have implemented file upload following this answer in Ionic Forum, using an IonButton and an input.
<input
type="file"
id="file-upload"
style={{ display: "none" }}
onChange={() => { setFile(index);}}/>
<IonButton
onClick={() => {openFileDialog();}}
disabled={files === undefined || files[index] === undefined}>
<IonLabel>Upload file</IonLabel>
<IonIcon slot="start" />
</IonButton>
Code:
function openFileDialog() {
(document as any).getElementById("file-upload").click();
}
const setFile = (index: number) => (_event: any) => {
console.log(`Getting file for index ${index}`);
let f = _event.target.files![0];
var reader = new FileReader();
reader.onload = function () {
setValue(`files.${index}.file`, reader.result);
};
reader.onerror = function () {
console.log("File load failed");
};
reader.readAsDataURL(f);
};
Full example code: codesandbox
The file is correctly uploaded but I have not been able to add it to the correct field in the FieldArray. Files are always added to element 0. I assume this is related to input not been modified directly in the form but in function openFileDialog(). As a result, the function onChange() of input does not receive the correct value of index.
Is it or is there a different source of error?
A solution would be to wait for the file to be loaded in the method onClick() of IonButton but I cannot send the index when calling (document as any).getElementById("file-upload").click();.
Another solution could be to use only one component for file upload instead of two. However, it looks like Ionic does not have a component for this. IonInput type="file" does not work. The documentation is confusing: "file" does not appear in the list of accepted values for property type but it is mentioned in the description of properties multiple and accepted.
How can I save the file correctly?
I found a few issues with your approach, I also removed the reading of the file into a blob, dont think you should do that until the user actually submits since they could delete the file.
First Issue - you were not passing index into this function
const setFile = (index: number, _event: any) => {
methods.setValue(`files[${index}].file` as any, _event.target.files[0]);
methods.setValue(
`files[${index}].title` as any,
_event.target.files[0]?.name
);
};
The second issue, you were not creating unique identifiers for the upload button, you need to include the index there also
<IonItem>
<input
type="file"
id={`file-upload-${index}`} // <== NEW CODE
style={{ display: "none" }}
onChange={(e) => {
console.log("In input", index);
setFile(index, e);
}}
/>
<IonButton
onClick={() => {
openFileDialog(index); // <== NEW CODE
}}
disabled={files === undefined || files[index] === undefined}
>
<IonLabel>Upload file</IonLabel>
<IonIcon slot="start" />
</IonButton>
</IonItem>
and then you need the index in openFileDialog so you can click the appropriate button.
function openFileDialog(index: any) {
(document as any).getElementById("file-upload-" + index).click();
}
See the complete solution here in this sandbox
https://codesandbox.io/s/ionic-react-hook-form-array-with-file-input-nq7t7

Test document listener with React Testing Library

I'm attempting to test a React component similar to the following:
import React, { useState, useEffect, useRef } from "react";
export default function Tooltip({ children }) {
const [open, setOpen] = useState(false);
const wrapperRef = useRef(null);
const handleClickOutside = (event) => {
if (
open &&
wrapperRef.current &&
!wrapperRef.current.contains(event.target)
) {
setOpen(false);
}
};
useEffect(() => {
document.addEventListener("click", handleClickOutside);
return () => {
document.removeEventListener("click", handleClickOutside);
};
});
const className = `tooltip-wrapper${(open && " open") || ""}`;
return (
<span ref={wrapperRef} className={className}>
<button type="button" onClick={() => setOpen(!open)} />
<span>{children}</span>
<br />
<span>DEBUG: className is {className}</span>
</span>
);
}
Clicking on the tooltip button changes the state to open (changing the className), and clicking again outside of the component changes it to closed.
The component works (with appropriate styling), and all of the React Testing Library (with user-event) tests work except for clicking outside.
it("should close the tooltip on click outside", () => {
// Arrange
render(
<div>
<p>outside</p>
<Tooltip>content</Tooltip>
</div>
);
const button = screen.getByRole("button");
userEvent.click(button);
// Temporary assertion - passes
expect(button.parentElement).toHaveClass("open");
// Act
const outside = screen.getByText("outside");
// Gives should be wrapped into act(...) warning otherwise
act(() => {
userEvent.click(outside);
});
// Assert
expect(button.parentElement).not.toHaveClass("open"); // FAILS
});
I don't understand why I had to wrap the click event in act - that's generally not necessary with React Testing Library.
I also don't understand why the final assertion fails. The click handler is called twice, but open is true both times.
There are a bunch of articles about limitations of React synthetic events, but it's not clear to me how to put all of this together.
I finally got it working.
it("should close the tooltip on click outside", async () => {
// Arrange
render(
<div>
<p data-testid="outside">outside</p>
<Tooltip>content</Tooltip>
</div>
);
const button = screen.getByRole("button");
userEvent.click(button);
// Verify initial state
expect(button.parentElement).toHaveClass("open");
const outside = screen.getByTestId("outside");
// Act
userEvent.click(outside);
// Assert
await waitFor(() => expect(button.parentElement).not.toHaveClass("open"));
});
The key seems to be to be sure that all activity completes before the test ends.
Say a test triggers a click event that in turn sets state. Setting state typically causes a rerender, and your test will need to wait for that to occur. Normally you do that by waiting for the new state to be displayed.
In this particular case waitFor was appropriate.

Resources