'How to test react component based on file reader emitted events

I have a react component called FileReader based off Dropzone react component (https://github.com/react-dropzone/react-dropzone). This file reader calls a callback react hook upon dropping a file on the input element of FileReader which looks like so:

import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
import { useDropzone } from 'react-dropzone';

function FilePicker(props) {
  const { fileName, setFile, setFileName } = props;

  const onDrop = useCallback((acceptedFiles) => {
    const reader = new FileReader();

    reader.onload = () => {
      const binaryFile = reader.result;
      setFile(binaryFile);
    };

    acceptedFiles.forEach((file) => {
      setFileName(file.name);
      return reader.readAsBinaryString(file);
    });
  }, [setFile, setFileName]);
  const { getRootProps, getInputProps } = useDropzone({ onDrop });

  return (
    <div data-testid="file-picker" id="file-picker" {...getRootProps()}>
      <div className="container-fluid inherit-height">
        <input data-testid="file-picker-input" {...getInputProps()} />
        <div className="row align-content-center inherit-height">
          <p data-testid={fileName} className="text-center" id="file-picker-title">
            {fileName !== '' ? fileName : 'Drag n drop a file here, or click to select file' }
          </p>
        </div>
      </div>
    </div>
  );
}

FilePicker.propTypes = {
  fileName: PropTypes.string.isRequired,
  setFile: PropTypes.func.isRequired,
  setFileName: PropTypes.func.isRequired,
};

export default FilePicker;

Now I am trying to test this component in a way in which I could check if the functions setFile and setFileName have been called appropriately. My test file looks like so:

import React from 'react';
import { render, cleanup, fireEvent, act } from '@testing-library/react';
import 'jest-dom/extend-expect';
import FileReader from './__mocks__/FileReader';
import FilePicker from './FilePicker';

afterEach(cleanup);

describe('React Use S3 Demo', () => {
  it('should render file picker', () => {
    const mockedFileName = '';
    const mockedSetFile = jest.fn();
    const mockedSetFileName = jest.fn();

    const { rerender, getByTestId, debug } = render(
      <FilePicker
        fileName={mockedFileName}
        setFile={mockedSetFile}
        setFileName={mockedSetFileName}
      />,
    );

    const filePicker = getByTestId('file-picker-input');

    act(() => {
      fireEvent.drop(filePicker, {
        target: {
          files: [new File(['(⌐□_□)'], 'chucknorris.png', { type: 'image/png' })],
        },
      });
    });

    expect(mockedSetFileName).toHaveBeenCalled();
    expect(mockedSetFile).toHaveBeenCalled();
  });
});

At first glance it seems like this would work but in reality it doesn't as the FileReader component doesn't seem to call the two functions that are coming down as props. I have tried using the rerender function from '@testing-library/react' but that does not seem to help either. How would you go about testing FileReader in order to have your tests validated upon the setFile and setFileName functions being called?

Thanks!



Solution 1:[1]

I found a way to trigger the reader.onload in my test.

The product I was running into, was that when I mocked the FileReader, the onload handler was never firing. So I created a mockFileReader which calls into the onload handler when readAsDataURL is called. You could easily use any of the readAs methods, depending on what you're using in your product code.

Here's my product code (simplified)

import * as React from 'react'
import { uploadPhoto } from 'services/photos'
import { PhotoDropzone } from 'modules/PhotoDropzone'

export const UploadPhotoComponent: React.FC = () => {
  const onDrop = React.useCallback((acceptedFiles: File[]) => {
    const reader = new FileReader()
  
    reader.onload = () => {
      uploadPhoto({encodedPhoto: reader.result?.toString()})
    }
    reader.readAsDataURL(acceptedFiles[0]
  }
  
  return <PhotoDropzone aria-label='Upload photo' onDrop={onDrop} />
}

And here's my test code:

import * as photosService from 'services/photos'
import { successfulAxiosResponse } from 'mocks/axios'
import { mockFileReader } from 'mocks/fileReader'
import { screen, render, fireEvent } from '@testing-library/react'

it('upload gets called, when file dropped on dropzone', async () => {
  const uploadProfilePhotoSpy = jest.spyOn(photosService, 'uploadPhoto')
    .mockResolvedValue(successfulAxiosResponse)

  const fileReaderConstructorSpy = jestspyOn(global, 'FileReader')
    .mockReturnValue(mockFileReader)

  const file = new File(['fake file contents'], 'profilePhoto.png', {
    type: 'image/png',
  })

  setup() // this does the render
  const uploadInput = screen.getByLabelText('Upload profile photo')
  fireEvent.drop(uploadInput, {files: [file]})

  await waitFor(() => expect(fileReaderConstructorSpy).toHaveBeenCalledTimes(1))
  mockFileReader.onload = jest.fn()
  await waitFor(() => expect(uploadProfilePhotoSpy).toHaveReturnedTimes(1))
}

And here is my mockFileReader, in case that's helpful:

const mockedReadAsDataURL = jest.fn().mockImplementation((args) =>
  mockFileReader.onload && mockFileReader.onload(args))

export const mockFileReader: FileReader = {
  abort: jest.fn(),
  addEventListener: jest.fn(),
  dispatchEvent: jest.fn(),
  DONE: 0,
  EMPTY: 0,
  error: null,
  LOADING: 0,
  onabort: jest.fn(),
  onerror: jest.fn(),
  onload: jest.fn(),
  onloadend: jest.fn(),
  onloadstart: jest.fn(),
  onprogress: jest.fn(),
  readAsArrayBuffer: jest.fn(),
  readAsBinaryString: jest.fn(),
  readAsDataURL: mockedReadAsDataURL,
  readAsText: jest.fn(),
  readyState: 0,
  removeEventListener: jest.fn(),
  result: null,
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 ThePuzzleMaster