react-testing-library - how to simulate file upload to a <input type="file" /> element?

Viewed 27971

I am using user-event to try to have more 'realistic' user interactions. However, after I click on the input, it will not fire the onChange function because by default it would only bring up the file explorer for the user to upload a file. How do I simulate the user uploading a file?

My code:

// Component
const FileInputComponent = ({ handleFileUpload }) => (
  <div>
    <input type="file" id="testing" accept=".png,.jpg" onChange={handleFileUpload} />
    <label htmlFor="testing">Input File Here</label>
  </div>
);
// Test file
test("Clicking on the label button calls the `handleFileUpload` function", () => {
  const handleFileUploadMockFn = jest.fn();
  const { getByLabelText } = render(<FileInputComponent handleFileUpload={handleFileUploadMockFn} />
  userEvent.click(getByLabelText("Input File Here"));
  expect(handleFileUploadMockFn).toHaveBeenCalledTimes(1);
});
5 Answers

I found a bit hacky solution and I'm not sure if it match the best practises in testing but I will share it with you it might help

describe("Upload files", () => {
  let file;

  beforeEach(() => {
    file = new File(["(⌐□_□)"], "chucknorris.png", { type: "image/png" });
  });

  test("cover photo upload", async () => {
    // render the component
    const { getByTestId } = render(<YourComponent />);

    // get the upload button
    let uploader = getByTestId("photo-uploader");

    // simulate ulpoad event and wait until finish
    await waitFor(() =>
      fireEvent.change(uploader, {
        target: { files: [file] },
      })
    );
    // get the same uploader from the dom
    let image = document.getElementById("photo-uploader");
    // check if the file is there
    expect(image.files[0].name).toBe("chucknorris.png");
    expect(image.files.length).toBe(1);
  });
});

Input Element -

<input type="file" data-testid="fileDropzone"/>

First, render the element, then get the input file upload element by getByTestId.

Secondly, define a fake file.

const fakeFile = new File(['hello'], 'hello.png', { type: 'image/png' });

Then, import userEvent. and upload the file, make sure to rap inside act().

import userEvent from '@testing-library/user-event';

The test the expectation, here is the final code.

it('Upload Files', async () => {
  const { getByTestId } = render(<FileUplaodComponent/>);
  const fakeFile = new File(['hello'], 'hello.png', { type: 'image/png' });
  const inputFile = getByTestId(/fileDropzone/i);
  await act(async () => {
       await waitFor(() => {
            userEvent.upload(inputFile, fakeFile);
       });
   });
  expect(inputFile.files[0]).toStrictEqual(inputFile);
 });

The answer by @darthzeren is nearly correct but the link seems outdated. Here is the solution described by lib maintainers: https://testing-library.com/docs/ecosystem-user-event/#uploadelement-file--clickinit-changeinit--options

Just to quote example from docs, in case the link does not work in the future:

import React from 'react'
import {render, screen} from '@testing-library/react'
import userEvent from '@testing-library/user-event'

test('upload file', () => {
  const file = new File(['hello'], 'hello.png', {type: 'image/png'})

  render(
    <div>
      <label htmlFor="file-uploader">Upload file:</label>
      <input id="file-uploader" type="file" />
    </div>,
  )
  const input = screen.getByLabelText(/upload file/i)
  userEvent.upload(input, file)

  expect(input.files[0]).toStrictEqual(file)
  expect(input.files.item(0)).toStrictEqual(file)
  expect(input.files).toHaveLength(1)
})

test('upload multiple files', () => {
  const files = [
    new File(['hello'], 'hello.png', {type: 'image/png'}),
    new File(['there'], 'there.png', {type: 'image/png'}),
  ]

  render(
    <div>
      <label htmlFor="file-uploader">Upload file:</label>
      <input id="file-uploader" type="file" multiple />
    </div>,
  )
  const input = screen.getByLabelText(/upload file/i)
  userEvent.upload(input, files)

  expect(input.files).toHaveLength(2)
  expect(input.files[0]).toStrictEqual(files[0])
  expect(input.files[1]).toStrictEqual(files[1])
})

This is how we test a file input with RTL :

describe('input file', () => {
  let rtl: RenderResult;

  beforeEach(() => {
    rtl = render(<MyComponentWithFileInput />);
  });

  test('input file', async () => {
    const file = new File(['(⌐□_□)'], 'file.xml', { type: 'application/xml' });

    // upload the file by updating the value attribute of the input
    // I assume : <input type="file" data-testid="fileInput" />
    fireEvent.change(rtl.getByTestId('fileInput'), {
      target: { files: [file] },
    });

    // here your onChange function should have been called
    expect(handleFileUploadMockFn).toHaveBeenCalledTimes(1);

    // if you have a form which is submitted you should be able to check values :
    expect(onSubmitForm).toHaveBeenCalledWith({
      file: expect.any(Object),
    });
  });
});

This is the general idea, obviously this should be updated to match your setup.

Related