Testing File Upload Component with RTL

Testing File Upload Component with RTL

React Testing Library

React Testing Library is a testing framework for React applications that helps you test the behaviour of your components rather than their implementation details. It is built on top of DOM Testing Library, which is a set of utilities for testing web pages. The main advantage of using React Testing Library over other testing frameworks, such as Enzyme, is that it encourages testing in a way that is more similar to how a user interacts with the application.

File Upload Component

import React, { useState } from 'react';
interface Props {
    onSubmit: (file: File) => void;
}
const FileUpload: React.FC<Props> = ({ onSubmit }) => {
    const [file, setFile] = useState<File | null>(null);

    const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
        setFile(e.target.files?.[0]);
    };
    const handleSubmit = (e: React.FormEvent) => {
        e.preventDefault();
        if(file){
            onSubmit(file);
        }
    };
    return (
        <form onSubmit={handleSubmit}>
            <input
                type="file"
                onChange={handleFileSelect}
                data-testid="file-input"
            />
            {file && <p data-testid="file-name">{file.name}</p>}
            <button type="submit" data-testid="submit-button">
                Upload
            </button>
        </form>
    );
};
export default FileUpload;

This component uses React's useState hook to keep track of the selected file, and it has an input field of type "file" which allows the user to select a file. When the form is submitted, it calls the onSubmit function and passes the selected file as a parameter.

It also includes data-testid attributes in the input and button elements, so that you can select them in your tests with RTL's getByTestId function.

Testing the File Upload Component with RTL

To test a file upload component with React Testing Library (RTL) and TypeScript, you can follow these steps:

  1. Import the necessary modules: In addition to RTL, you will also need to import the component you want to test and any additional modules it depends on.

  2. Create the test: Use the render function from RTL to render the component, and the fireEvent function to simulate a file being selected and the form is submitted.

  3. Test the component's behaviour: Once the component is rendered, you can use RTL's getBy* functions to select elements within the component and make assertions about their behaviour. For example, you can check that the component displays the selected file's name and that the form's onSubmit function is called when the form is submitted.

  4. Test the component's state: you can use the debug() function to check the component's state, it will show the component's html code and you can check that the component's state is updated correctly when the file is selected and the form is submitted.

Here is an example of how your test file might look like:

import { render, fireEvent, waitFor, debug } from '@testing-library/react';
import FileUpload from './fileUpload';

describe('FileUpload', () => {
    it('should display the selected file name', async () => {
        const { getByTestId } = render(<FileUpload />);

        const fileInput = getByTestId('file-input');
        const file = new File(['test file content'], 'test.txt', {
            type: 'text/plain',
        });
        fireEvent.change(fileInput, { target: { files: [file] } });

        const fileName = getByTestId('file-name');
        expect(fileName.textContent).toBe('test.txt');
    });

    it('should call the onSubmit function when the form is submitted', async () => {
        const onSubmit = jest.fn();
        const { getByTestId } = render(<FileUpload onSubmit={onSubmit} />);

        const fileInput = getByTestId('file-input');
        const file = new File(['test file content'], 'test.txt', {
            type: 'text/plain',
        });
        fireEvent.change(fileInput, { target: { files: [file] } });

        const submitButton = getByTestId('submit-button');
        fireEvent.click(submitButton);

        expect(onSubmit).toHaveBeenCalled();
        debug()
    });
});

Keep in mind that you should focus on testing the component's behavior rather than its implementation details. You should also test the component with different kind of files, empty files, or even no files at all.

File Upload Component that accepts only JPEG

import React, { useState } from 'react';

interface Props {
    onSubmit: (file: File) => void;
}
const FileUpload: React.FC<Props> = ({ onSubmit }) => {
    const [file, setFile] = useState<File | null>(null);
    const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
        const selectedFile = e.target.files?.[0];
        if (selectedFile && selectedFile.type === 'image/jpeg') {
            setFile(selectedFile);
        }
    };

    const handleSubmit = (e: React.FormEvent) => {
        e.preventDefault();
        if(file){
            onSubmit(file);
        }
    };

    return (
        <form onSubmit={handleSubmit}>
            <input
                type="file"
                accept="image/jpeg"
                onChange={handleFileSelect}
                data-testid="file-input"
            />
            {file && <p data-testid="file-name">{file.name}</p>}
            <button type="submit" data-testid="submit-button">
                Upload
            </button>
        </form>
    );
};
export default FileUpload;

In this version, I added an accept attribute to the file input element, which tells the browser to only display files that are of type image/jpeg.

I also added a check in the handleFileSelect function to make sure the selected file is of type 'image/jpeg' before setting the state.

In this way, you ensure that the user can only select JPEG files, and that the component only accepts and processes JPEG files, so you can be sure that you are working with the correct type of file.

Testing the modified File Upload Component

import { render, fireEvent, waitFor, debug } from '@testing-library/react';
import FileUpload from './fileUpload'

describe('FileUpload', () => {
    it('should display the selected file name', async () => {
        const { getByTestId } = render(<FileUpload />);

        const fileInput = getByTestId('file-input');
        const file = new File(['test file content'], 'test.jpg', {
            type: 'image/jpeg',
        });
        fireEvent.change(fileInput, { target: { files: [file] } });

        const fileName = getByTestId('file-name');
        expect(fileName.textContent).toBe('test.jpg');
    });

    it('should not display the selected file name and not call the onSubmit function when the file is not JPEG', async () => {
        const onSubmit = jest.fn();
        const { getByTestId } = render(<FileUpload onSubmit={onSubmit} />);

        const fileInput = getByTestId('file-input');
        const file = new File(['test file content'], 'test.txt', {
            type: 'text/plain',
        });
        fireEvent.change(fileInput, { target: { files: [file] } });

        const submitButton = getByTestId('submit-button');
        fireEvent.click(submitButton);

        const fileName = getByTestId('file-name');
        expect(fileName).toBe(null);
        expect(onSubmit).not.toHaveBeenCalled();
    });

    it('should call the onSubmit function when the form is submitted', async () => {
        const onSubmit = jest.fn();
        const { getByTestId } = render(<FileUpload onSubmit={onSubmit} />);

        const fileInput = getByTestId('file-input');
        const file = new File(['test file content'], 'test.jpg', {
            type: 'image/jpeg',
        });
        fireEvent.change(file
        fireEvent.change(fileInput, { target: { files: [file] } });

        const submitButton = getByTestId('submit-button');
        fireEvent.click(submitButton);

        expect(onSubmit).toHaveBeenCalled();
        debug()
    });
});

In this test case, I added a test that simulates the user selecting a file that is not of type image/jpeg, and asserts that the component does not display the file name and does not call the onSubmit function.

I also updated the existing test case that selects a file to use a JPEG file instead of a plain text file. And also the test case asserts that the component displays the selected file name and that the form's onSubmit function is called when the form is submitted.

It is also a good idea to add test cases that cover edge cases, like when the user tries to submit the form without selecting a file, or when the user selects multiple files.

Please keep in mind that this is just an example, and you will need to adjust the test to match the specific implementation of your component.

Conclusion

In conclusion, testing file upload components can be a bit tricky, as it involves both the component's behaviour and the file handling. To ensure that the component is working correctly, it is important to test the component's behaviour with different types of files, including valid and invalid file types, and also test the component's behaviour when the user tries to submit the form without selecting a file, or when the user selects multiple files.

React Testing Library (RTL) is a great tool for testing React components and it provides a lot of useful functions that help you test the component's behaviour.

It's worth noting that, when testing file upload components, you should focus on testing the component's behaviour rather than its implementation details.

I hope this blog post was helpful and if you liked it and found it helpful, please don't hesitate to hit the like button and follow me for more, also if you have any specific component you would like me to test with RTL feel free to comment and I will be happy to help.

Did you find this article valuable?

Support Welcome to Omar's Blog by becoming a sponsor. Any amount is appreciated!